package cn.kivensoft.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/** 针对系统路径和jar包进行统一资源处理的资源处理类
 * @author kiven lee
 * @version 1.0
 * @since 2016-09-21
 */
final public class Resources {
	public static final String LOGBACK = "logback-spring.xml";
	public static final String LOG4J = "log4j.properties";

	private static final String UTF8 = "UTF-8";
	private static final String LIB = "/lib";
	private static final String CONF = "/conf";
	private static final String ETC = "/etc";
	private static final String ROOT = "/";

	/** 从配置文件获取配置内容
	 * @param filename 配置文件名
	 * @param mainClass 配置文件所在的主类
	 * @param config 配置实例
	 * @return 配置对象
	 */
	public static <T> T loadToObject(String filename, Class<?> mainClass, T config) {
		Map<?, ?> props = loadProperties(filename, mainClass, LIB, CONF, ROOT);
		if (props != null) {
			@SuppressWarnings("unchecked")
			Map<String, String> map = transKey((Map<String, String>)props);
			BeanUtils.setFields(config, (name, type) -> Strings.valueOf(map.get(name), type));
		}
		return config;
	}

	/**
	 * @param filename 配置文件名，系统默认从根目录读取，最好只是不带路径的文件名
	 * @param mainClass 配置文件所在jar包的类
	 * @return 配置内容
	 */
	public static Properties loadProperties(String filename, Class<?> mainClass) {
		return loadProperties(filename, mainClass, LIB, ROOT, CONF, ETC);
	}

	/** 加载配置文件, 优先以普通文件方式加载, 找不到则尝试以资源文件方式加载
	 * @param filename 文件名
	 * @param mainClass 应用程序main函数入库所在的类
	 * @param lib lib所在目录, 应用可以在lib下, 也可以在根目录下
	 * @param dirs 尝试加载的子目录数组, 按顺序优先
	 * @return 配置文件全路径名称及配置文件内容, 加载失败则抛出异常
	 */
	public static Properties loadProperties(String filename, Class<?> mainClass, String lib,
			String... dirs) {
		InputStream is = getResourceAsStream(filename, mainClass, lib, dirs);
		if (is == null) return null;
		try (InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8)){
			Properties props = new Properties();
			props.load(reader);
			return props;
		} catch (Exception e) {
			return null;
		} finally {
			try { is.close(); } catch (IOException ignored) {}
		}
	}

	/** 获取程序路径，主要是要区分部署与开发之间的路径区别 
	 * @param mainClass 应用程序main函数入库所在的类
	 * @return 根目录
	 */
	public static String getRootPath(Class<?> mainClass) {
		return getRootPath(mainClass, LIB);
	}

	/** 获取程序路径，主要是要区分部署与开发之间的路径区别 
	 * @param mainClass 应用程序main函数入库所在的类
	 * @param lib 如果是jar包, 指定jar包所在的相对子路径, 可选项
	 * @return 根目录
	 */
	public static String getRootPath(Class<?> mainClass, String lib) {
		// 获取类所在的路径, 返回的可能是直接的路径或者jar包的全路径名
		URL url = mainClass.getProtectionDomain().getCodeSource().getLocation();
		String path;
		try {
			path = java.net.URLDecoder.decode(url.getPath(), UTF8);
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException("UnsupportedEncodingException", e);
		}

		if(path.endsWith(".jar")) { //部署环境
			path = path.substring(0, path.lastIndexOf('/'));
			if (lib != null && !lib.isEmpty()) {
				lib = Strings.joinPath("/", lib);
				if (path.endsWith(lib))
					path = path.substring(0, path.length() - lib.length());
			}
		}

		return path;
	}

	/** 加载并初始化log4j配置文件log4j.properties, 优先以普通文件方式加载, 找不到则尝试以资源文件方式加载
	 * @param mainClass 应用程序main函数入库所在的类
	 * @return 加载的配置文件全路径名称
	 */
	public static String initLog4j(Class<?> mainClass) throws Exception {
		return initLog4j(LOG4J, mainClass, LIB, ROOT, CONF, ETC);
	}

	/** 加载并初始化log4j配置文件, 优先以普通文件方式加载, 找不到则尝试以资源文件方式加载
	 * @param filename 文件名
	 * @param mainClass 应用程序main函数入库所在的类
	 * @return 加载的配置文件全路径名称
	 */
	public static String initLog4j(String filename, Class<?> mainClass) throws Exception {
		return initLog4j(LOG4J, mainClass, LIB, ROOT, CONF, ETC);
	}

	/** 加载并初始化log4j配置文件, 优先以普通文件方式加载, 找不到则尝试以资源文件方式加载
	 * @param filename 文件名
	 * @param mainClass 应用程序main函数入库所在的类
	 * @param lib lib所在目录, 应用可以在lib下, 也可以在根目录下
	 * @param dirs 尝试加载的子目录数组, 按顺序优先
	 * @return 加载的配置文件全路径名称
	 */
	public static String initLog4j(String filename, Class<?> mainClass, String lib,
			String... dirs) throws Exception {
		Pair<String, InputStream> si = _getResourceAsStream(filename, mainClass, lib, dirs);
		if (si.getSecond() == null)
			throw new Exception(Fmt.fmt("load file {} error.", filename));

		InputStreamReader reader = new InputStreamReader(si.getSecond(), StandardCharsets.UTF_8);
		Properties props = new Properties();
		props.load(reader);

		Class.forName("org.apache.log4j.PropertyConfigurator")
			.getMethod("configure", Properties.class)
			.invoke(null, props);

		return si.getFirst();
	}

	/** 加载并初始化logback配置文件logback.xml, 优先以普通文件方式加载, 找不到则尝试以资源文件方式加载
	 * @param mainClass 应用程序main函数入库所在的类
	 * @return 加载的配置文件全路径名称
	 */
	public static String initLogback(Class<?> mainClass) throws Exception {
		return initLogback(LOGBACK, mainClass, LIB, ROOT, CONF, ETC);
	}

	/** 加载并初始化logback配置文件, 优先以普通文件方式加载, 找不到则尝试以资源文件方式加载
	 * @param filename 文件名
	 * @param mainClass 应用程序main函数入库所在的类
	 * @return 加载的配置文件全路径名称
	 */
	public static String initLogback(String filename, Class<?> mainClass) throws Exception {
		return initLogback(filename, mainClass, LIB, ROOT, CONF, ETC);
	}

	/** 加载并初始化logback配置文件, 优先以普通文件方式加载, 找不到则尝试以资源文件方式加载
	 * @param filename 文件名
	 * @param mainClass 应用程序main函数入库所在的类
	 * @param lib lib所在目录, 应用可以在lib下, 也可以在根目录下
	 * @param dirs 尝试加载的子目录数组, 按顺序优先
	 * @return 加载的配置文件全路径名称
	 */
	public static String initLogback(String filename, Class<?> mainClass, String lib,
			String... dirs) throws Exception {
		Pair<String, InputStream> si = _getResourceAsStream(filename, mainClass, lib, dirs);
		if (si.getSecond() == null)
			throw new Exception(Fmt.fmt("load file {} error.", filename));

		Class<?> factory_cls = Class.forName("org.slf4j.LoggerFactory");
		Class<?> lctx_cls = Class.forName("ch.qos.logback.classic.LoggerContext");
		Object loggerContext = factory_cls.getMethod("getILoggerFactory").invoke(null);
		lctx_cls.getMethod("reset").invoke(loggerContext);

		Class<?> ctx_cls = Class.forName("ch.qos.logback.core.Context");
		Class<?> conf_cls = Class.forName("ch.qos.logback.classic.joran.JoranConfigurator");
		Object joranConfigurator = conf_cls.newInstance();
		conf_cls.getMethod("setContext", ctx_cls).invoke(joranConfigurator, loggerContext);
		conf_cls.getMethod("doConfigure", InputStream.class)
				.invoke(joranConfigurator, si.getSecond());
		return si.getFirst();
	}

	/** 加载资源文件, 优先以普通文件方式加载, 找不到则尝试以资源文件方式加载
	 * @param filename 文件名
	 * @param mainClass 应用程序main函数入库所在的类
	 * @return 文件的输入流, 为null则加载失败
	 */
	public static InputStream getResourceAsStream(String filename, Class<?> mainClass) {
		return getResourceAsStream(filename, mainClass, LIB, ROOT, CONF, ETC);
	}

	/** 加载资源文件, 优先以普通文件方式加载, 找不到则尝试以资源文件方式加载
	 * @param filename 文件名
	 * @param mainClass 应用程序main函数入库所在的类
	 * @param lib lib所在目录, 应用可以在lib下, 也可以在根目录下
	 * @param dirs 尝试加载的子目录数组, 按顺序优先
	 * @return 文件的输入流, 为null则加载失败
	 */
	public static InputStream getResourceAsStream(String filename, Class<?> mainClass,
			String lib, String... dirs) {
		return _getResourceAsStream(filename, mainClass, lib, dirs).getSecond();
	}

	private static Pair<String, InputStream> _getResourceAsStream(String filename,
			Class<?> mainClass, String lib, String... dirs) {
		int len = dirs.length;
		String rootPath = getRootPath(mainClass, lib);
		// 优先加载磁盘路径下的文件
		for (String dir : dirs) {
			File f = new File(Strings.joinPath(rootPath, dir, filename));
			if (f.exists()) try {
				return Pair.of(f.getAbsolutePath(), new FileInputStream(f));
			} catch (Exception e) {
				return Pair.of(f.getAbsolutePath(), null);
			}
		}
		// 尝试以加载资源的方式加载文件
		for (String dir : dirs) {
			String p = Strings.joinPath(ROOT, dir, filename);
			InputStream is = mainClass.getResourceAsStream(p);
			if (is != null) return Pair.of(p, is);
		}
		return Pair.of(null, null);
	}

	/** 查找资源文件, 优先查找普通文件方式, 找不到则尝试查找资源文件
	 * @param filename 文件名
	 * @param mainClass 应用程序main函数入库所在的类
	 * @return 找到的文件全路径名, '/'开头表示是资源文件, 为null则查找失败
	 */
	public static String findResource(String filename, Class<?> mainClass) {
		return findResource(filename, mainClass, LIB, ROOT, CONF, ETC);
	}

	/** 查找资源文件, 优先查找普通文件方式, 找不到则尝试查找资源文件
	 * @param filename 文件名
	 * @param mainClass 应用程序main函数入库所在的类
	 * @param lib lib所在目录, 应用可以在lib下, 也可以在根目录下
	 * @param dirs 尝试查找的子目录数组, 按顺序优先
	 * @return 找到的文件全路径名, '/'开头表示是资源文件, 为null则查找失败
	 */
	public static String findResource(String filename, Class<?> mainClass, String lib,
			String... dirs) {
		if (filename == null || filename.isEmpty()) return null;

		int len = dirs.length;
		String rootPath = getRootPath(mainClass, lib);

		// 查找磁盘路径下的文件
		for (String dir : dirs) {
			File f = new File(Strings.joinPath(rootPath, dir, filename));
			if (f.exists()) return f.getAbsolutePath();
		}

		// 查找资源文件
		for (String dir : dirs) {
			String path = Strings.joinPath(ROOT, dir, filename);
			InputStream is = mainClass.getResourceAsStream(path);
			if (is != null) {
				try {
					is.close();
				} catch (Exception ignored) {
				}
				return path;
			}
		}

		return null;
	}

	/** 将源字典中的键名做标准Java Bean名字的转换, 将"."忽略, 并把之后跟随的字符大写
	 * @param map 源字典
	 * @return 新的map，键名已做转换
	 */
	public static HashMap<String, String> transKey(Map<String, String> map) {
		HashMap<String, String> ret = new HashMap<>();
		StringBuilder sb = new StringBuilder();
		for (Map.Entry<String, String> entry : map.entrySet()) {
			String key = entry.getKey();
			sb.setLength(0);
			sb.append(key.charAt(0));
			boolean dot = false;
			for (int i = 1, imax = key.length(); i < imax; i++) {
				char c = key.charAt(i);
				if (c == '.') dot = true;
				else if (dot) {
					sb.append(Character.toUpperCase(c));
					dot = false;
				}
				else sb.append(c);
			}
			ret.put(sb.toString(), entry.getValue());
		}
		return ret;
	}

	public static void main(String[] args) throws Exception {
		if (args.length == 0) {
			System.out.println("usage: program <className>");
			return;
		}
		String p = getRootPath(Class.forName(args[0]));
		System.out.println("root path = " + p);
	}
}
